 /*******************************************************************************
  * Copyright (c) 2005, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/

 package org.eclipse.core.commands;

 import java.util.ArrayList ;
 import java.util.Collection ;
 import java.util.Collections ;
 import java.util.HashMap ;
 import java.util.Iterator ;
 import java.util.List ;
 import java.util.Map ;

 import org.eclipse.core.commands.common.NotDefinedException;
 import org.eclipse.core.internal.commands.util.Util;

 /**
  * <p>
  * A command that has had one or more of its parameters specified. This class
  * serves as a utility class for developers that need to manipulate commands
  * with parameters. It handles the behaviour of generating a parameter map and a
  * human-readable name.
  * </p>
  *
  * @since 3.1
  */
 public final class ParameterizedCommand implements Comparable {

     /**
      * The constant integer hash code value meaning the hash code has not yet
      * been computed.
      */
     private static final int HASH_CODE_NOT_COMPUTED = -1;

     /**
      * A factor for computing the hash code for all parameterized commands.
      */
     private static final int HASH_FACTOR = 89;

     /**
      * The seed for the hash code for all parameterized commands.
      */
     private static final int HASH_INITIAL = ParameterizedCommand.class
             .getName().hashCode();

     /**
      * The index of the parameter id in the parameter values.
      *
      * @deprecated no longer used
      */
     public static final int INDEX_PARAMETER_ID = 0;

     /**
      * The index of the human-readable name of the parameter itself, in the
      * parameter values.
      *
      * @deprecated no longer used
      */
     public static final int INDEX_PARAMETER_NAME = 1;

     /**
      * The index of the human-readable name of the value of the parameter for
      * this command.
      *
      * @deprecated no longer used
      */
     public static final int INDEX_PARAMETER_VALUE_NAME = 2;

     /**
      * The index of the value of the parameter that the command can understand.
      *
      * @deprecated no longer used
      */
     public static final int INDEX_PARAMETER_VALUE_VALUE = 3;

     /**
      * Escapes special characters in the command id, parameter ids and parameter
      * values for {@link #serialize()}. The special characters
      * {@link CommandManager#PARAMETER_START_CHAR},
      * {@link CommandManager#PARAMETER_END_CHAR},
      * {@link CommandManager#ID_VALUE_CHAR},
      * {@link CommandManager#PARAMETER_SEPARATOR_CHAR} and
      * {@link CommandManager#ESCAPE_CHAR} are escaped by prepending a
      * {@link CommandManager#ESCAPE_CHAR} character.
      *
      * @param rawText
      * a <code>String</code> to escape special characters in for
      * serialization.
      * @return a <code>String</code> representing <code>rawText</code> with
      * special serialization characters escaped
      * @see CommandManager#unescape(String)
      * @since 3.2
      */
     private static final String escape(final String rawText) {

         // defer initialization of a StringBuffer until we know we need one
 StringBuffer buffer = null;

         for (int i = 0; i < rawText.length(); i++) {

             char c = rawText.charAt(i);
             switch (c) {
             case CommandManager.PARAMETER_START_CHAR:
             case CommandManager.PARAMETER_END_CHAR:
             case CommandManager.ID_VALUE_CHAR:
             case CommandManager.PARAMETER_SEPARATOR_CHAR:
             case CommandManager.ESCAPE_CHAR:
                 if (buffer == null) {
                     buffer = new StringBuffer (rawText.substring(0, i));
                 }
                 buffer.append(CommandManager.ESCAPE_CHAR);
                 buffer.append(c);
                 break;
             default:
                 if (buffer != null) {
                     buffer.append(c);
                 }
                 break;
             }

         }

         if (buffer == null) {
             return rawText;
         }
         return buffer.toString();
     }

     /**
      * Generates every possible combination of parameter values for the given
      * parameters. Parameters values that cannot be initialized are just
      * ignored. Optional parameters are considered.
      *
      * @param startIndex
      * The index in the <code>parameters</code> that we should
      * process. This must be a valid index.
      * @param parameters
      * The parameters in to process; must not be <code>null</code>.
      * @return A collection (<code>Collection</code>) of combinations (<code>List</code>
      * of <code>Parameterization</code>).
      */
     private static final Collection expandParameters(final int startIndex,
             final IParameter[] parameters) {
         final int nextIndex = startIndex + 1;
         final boolean noMoreParameters = (nextIndex >= parameters.length);

         final IParameter parameter = parameters[startIndex];
         final List parameterizations = new ArrayList ();
         if (parameter.isOptional()) {
             parameterizations.add(null);
         }
         
         IParameterValues values = null;
         try {
             values = parameter.getValues();
         } catch (final ParameterValuesException e) {
             if (noMoreParameters) {
                 return parameterizations;
             }

             // Make recursive call
 return expandParameters(nextIndex, parameters);
         }
         final Map parameterValues = values.getParameterValues();
         final Iterator parameterValueItr = parameterValues.entrySet()
                 .iterator();
         while (parameterValueItr.hasNext()) {
             final Map.Entry entry = (Map.Entry ) parameterValueItr.next();
             final Parameterization parameterization = new Parameterization(
                     parameter, (String ) entry.getValue());
             parameterizations.add(parameterization);
         }

         // Check if another iteration will produce any more names.
 final int parameterizationCount = parameterizations.size();
         if (noMoreParameters) {
             // This is it, so just return the current parameterizations.
 for (int i = 0; i < parameterizationCount; i++) {
                 final Parameterization parameterization = (Parameterization) parameterizations
                         .get(i);
                 final List combination = new ArrayList (1);
                 combination.add(parameterization);
                 parameterizations.set(i, combination);
             }
             return parameterizations;
         }

         // Make recursive call
 final Collection suffixes = expandParameters(nextIndex, parameters);
         while (suffixes.remove(null)) {
             // just keep deleting the darn things.
 }
         if (suffixes.isEmpty()) {
             // This is it, so just return the current parameterizations.
 for (int i = 0; i < parameterizationCount; i++) {
                 final Parameterization parameterization = (Parameterization) parameterizations
                         .get(i);
                 final List combination = new ArrayList (1);
                 combination.add(parameterization);
                 parameterizations.set(i, combination);
             }
             return parameterizations;
         }
         final Collection returnValue = new ArrayList ();
         final Iterator suffixItr = suffixes.iterator();
         while (suffixItr.hasNext()) {
             final List combination = (List ) suffixItr.next();
             final int combinationSize = combination.size();
             for (int i = 0; i < parameterizationCount; i++) {
                 final Parameterization parameterization = (Parameterization) parameterizations
                         .get(i);
                 final List newCombination = new ArrayList (combinationSize + 1);
                 newCombination.add(parameterization);
                 newCombination.addAll(combination);
                 returnValue.add(newCombination);
             }
         }

         return returnValue;
     }

     /**
      * <p>
      * Generates all the possible combinations of command parameterizations for
      * the given command. If the command has no parameters, then this is simply
      * a parameterized version of that command. If a parameter is optional, both
      * the included and not included cases are considered.
      * </p>
      * <p>
      * If one of the parameters cannot be loaded due to a
      * <code>ParameterValuesException</code>, then it is simply ignored.
      * </p>
      *
      * @param command
      * The command for which the parameter combinations should be
      * generated; must not be <code>null</code>.
      * @return A collection of <code>ParameterizedCommand</code> instances
      * representing all of the possible combinations. This value is
      * never empty and it is never <code>null</code>.
      * @throws NotDefinedException
      * If the command is not defined.
      */
     public static final Collection generateCombinations(final Command command)
             throws NotDefinedException {
         final IParameter[] parameters = command.getParameters();
         if (parameters == null) {
             return Collections
                     .singleton(new ParameterizedCommand(command, null));
         }

         final Collection expansion = expandParameters(0, parameters);
         final Collection combinations = new ArrayList (expansion.size());
         final Iterator expansionItr = expansion.iterator();
         while (expansionItr.hasNext()) {
             final List combination = (List ) expansionItr.next();
             if (combination == null) {
                 combinations.add(new ParameterizedCommand(command, null));
             } else {
                 while (combination.remove(null)) {
                     // Just keep removing while there are null entries left.
 }
                 if (combination.isEmpty()) {
                     combinations.add(new ParameterizedCommand(command, null));
                 } else {
                     final Parameterization[] parameterizations = (Parameterization[]) combination
                             .toArray(new Parameterization[combination.size()]);
                     combinations.add(new ParameterizedCommand(command,
                             parameterizations));
                 }
             }
         }
         
         return combinations;
     }

     /**
      * The base command which is being parameterized. This value is never
      * <code>null</code>.
      */
     private final Command command;

     /**
      * The hash code for this object. This value is computed lazily, and marked
      * as invalid when one of the values on which it is based changes.
      */
     private transient int hashCode = HASH_CODE_NOT_COMPUTED;

     /**
      * This is an array of parameterization defined for this command. This value
      * may be <code>null</code> if the command has no parameters.
      */
     private final Parameterization[] parameterizations;

     private String name;

     /**
      * Constructs a new instance of <code>ParameterizedCommand</code> with
      * specific values for zero or more of its parameters.
      *
      * @param command
      * The command that is parameterized; must not be
      * <code>null</code>.
      * @param parameterizations
      * An array of parameterizations binding parameters to values for
      * the command. This value may be <code>null</code>. This
      * argument is not copied; if you need to make changes to it
      * after constructing this parameterized command, then make a
      * copy yourself.
      */
     public ParameterizedCommand(final Command command,
             final Parameterization[] parameterizations) {
         if (command == null) {
             throw new NullPointerException (
                     "A parameterized command cannot have a null command"); //$NON-NLS-1$
 }

         this.command = command;
         this.parameterizations = (parameterizations == null || parameterizations.length == 0) ? null
                 : parameterizations;
     }

     /*
      * (non-Javadoc)
      *
      * @see java.lang.Comparable#compareTo(java.lang.Object)
      */
     public final int compareTo(final Object object) {
         final ParameterizedCommand command = (ParameterizedCommand) object;
         final boolean thisDefined = this.command.isDefined();
         final boolean otherDefined = command.command.isDefined();
         if (!thisDefined || !otherDefined) {
             return Util.compare(thisDefined, otherDefined);
         }

         try {
             final int compareTo = getName().compareTo(command.getName());
             if (compareTo == 0) {
                 return getId().compareTo(command.getId());
             }
             return compareTo;
         } catch (final NotDefinedException e) {
             throw new Error (
                     "Concurrent modification of a command's defined state"); //$NON-NLS-1$
 }
     }

     /*
      * (non-Javadoc)
      *
      * @see java.lang.Object#equals(java.lang.Object)
      */
     public final boolean equals(final Object object) {
         if (this == object) {
             return true;
         }

         if (!(object instanceof ParameterizedCommand)) {
             return false;
         }

         final ParameterizedCommand command = (ParameterizedCommand) object;
         if (!Util.equals(this.command, command.command)) {
             return false;
         }

         return Util.equals(this.parameterizations, command.parameterizations);
     }

     /**
      * Executes this command with its parameters. This method will succeed
      * regardless of whether the command is enabled or defined. It is
      * preferrable to use {@link #executeWithChecks(Object, Object)}.
      *
      * @param trigger
      * The object that triggered the execution; may be
      * <code>null</code>.
      * @param applicationContext
      * The state of the application at the time the execution was
      * triggered; may be <code>null</code>.
      * @return The result of the execution; may be <code>null</code>.
      * @throws ExecutionException
      * If the handler has problems executing this command.
      * @throws NotHandledException
      * If there is no handler.
      * @deprecated Please use {@link #executeWithChecks(Object, Object)}
      * instead.
      */
     public final Object execute(final Object trigger,
             final Object applicationContext) throws ExecutionException,
             NotHandledException {
         return command.execute(new ExecutionEvent(command, getParameterMap(),
                 trigger, applicationContext));
     }

     /**
      * Executes this command with its parameters. This does extra checking to
      * see if the command is enabled and defined. If it is not both enabled and
      * defined, then the execution listeners will be notified and an exception
      * thrown.
      *
      * @param trigger
      * The object that triggered the execution; may be
      * <code>null</code>.
      * @param applicationContext
      * The state of the application at the time the execution was
      * triggered; may be <code>null</code>.
      * @return The result of the execution; may be <code>null</code>.
      * @throws ExecutionException
      * If the handler has problems executing this command.
      * @throws NotDefinedException
      * If the command you are trying to execute is not defined.
      * @throws NotEnabledException
      * If the command you are trying to execute is not enabled.
      * @throws NotHandledException
      * If there is no handler.
      * @since 3.2
      */
     public final Object executeWithChecks(final Object trigger,
             final Object applicationContext) throws ExecutionException,
             NotDefinedException, NotEnabledException, NotHandledException {
         return command.executeWithChecks(new ExecutionEvent(command,
                 getParameterMap(), trigger, applicationContext));
     }

     /**
      * Returns the base command. It is possible for more than one parameterized
      * command to have the same identifier.
      *
      * @return The command; never <code>null</code>, but may be undefined.
      */
     public final Command getCommand() {
         return command;
     }

     /**
      * Returns the command's base identifier. It is possible for more than one
      * parameterized command to have the same identifier.
      *
      * @return The command id; never <code>null</code>.
      */
     public final String getId() {
         return command.getId();
     }

     /**
      * Returns a human-readable representation of this command with all of its
      * parameterizations.
      *
      * @return The human-readable representation of this parameterized command;
      * never <code>null</code>.
      * @throws NotDefinedException
      * If the underlying command is not defined.
      */
     public final String getName() throws NotDefinedException {
         if (name == null) {
             final StringBuffer nameBuffer = new StringBuffer ();
             nameBuffer.append(command.getName());
             if (parameterizations != null) {
                 nameBuffer.append(" ("); //$NON-NLS-1$
 final int parameterizationCount = parameterizations.length;
                 for (int i = 0; i < parameterizationCount; i++) {
                     final Parameterization parameterization = parameterizations[i];
                     nameBuffer
                             .append(parameterization.getParameter().getName());
                     nameBuffer.append(": "); //$NON-NLS-1$
 try {
                         nameBuffer.append(parameterization.getValueName());
                     } catch (final ParameterValuesException e) {
                         /*
                          * Just let it go for now. If someone complains we can
                          * add more info later.
                          */
                     }

                     // If there is another item, append a separator.
 if (i + 1 < parameterizationCount) {
                         nameBuffer.append(", "); //$NON-NLS-1$
 }
                 }
                 nameBuffer.append(')');
             }
             name = nameBuffer.toString();
         }
         return name;
     }

     /**
      * Returns the parameter map, as can be used to construct an
      * <code>ExecutionEvent</code>.
      *
      * @return The map of parameter ids (<code>String</code>) to parameter
      * values (<code>String</code>). This map is never
      * <code>null</code>, but may be empty.
      */
     public final Map getParameterMap() {
         if ((parameterizations == null) || (parameterizations.length == 0)) {
             return Collections.EMPTY_MAP;
         }

         final Map parameterMap = new HashMap ();
         for (int i = 0; i < parameterizations.length; i++) {
             final Parameterization parameterization = parameterizations[i];
             parameterMap.put(parameterization.getParameter().getId(),
                     parameterization.getValue());
         }
         return parameterMap;
     }

     /* (non-Javadoc)
      * @see java.lang.Object#hashCode()
      */
     public final int hashCode() {
         if (hashCode == HASH_CODE_NOT_COMPUTED) {
             hashCode = HASH_INITIAL * HASH_FACTOR + Util.hashCode(command);
             hashCode = hashCode * HASH_FACTOR;
             if (parameterizations!=null) {
                 for (int i = 0; i < parameterizations.length; i++) {
                     hashCode += Util.hashCode(parameterizations[i]);
                 }
             }
             if (hashCode == HASH_CODE_NOT_COMPUTED) {
                 hashCode++;
             }
         }
         return hashCode;
     }

     /**
      * Returns a {@link String} containing the command id, parameter ids and
      * parameter values for this {@link ParameterizedCommand}. The returned
      * {@link String} can be stored by a client and later used to reconstruct an
      * equivalent {@link ParameterizedCommand} using the
      * {@link CommandManager#deserialize(String)} method.
      * <p>
      * The syntax of the returned {@link String} is as follows:
      * </p>
      *
      * <blockquote>
      * <code>serialization = <u>commandId</u> [ '(' parameters ')' ]</code><br>
      * <code>parameters = parameter [ ',' parameters ]</code><br>
      * <code>parameter = <u>parameterId</u> [ '=' <u>parameterValue</u> ]</code>
      * </blockquote>
      *
      * <p>
      * In the syntax above, sections inside square-brackets are optional. The
      * characters in single quotes (<code>(</code>, <code>)</code>,
      * <code>,</code> and <code>=</code>) indicate literal characters.
      * </p>
      * <p>
      * <code><u>commandId</u></code> represents the command id encoded with
      * separator characters escaped. <code><u>parameterId</u></code> and
      * <code><u>parameterValue</u></code> represent the parameter ids and
      * values encoded with separator characters escaped. The separator
      * characters <code>(</code>, <code>)</code>, <code>,</code> and
      * <code>=</code> are escaped by prepending a <code>%</code>. This
      * requires <code>%</code> to be escaped, which is also done by prepending
      * a <code>%</code>.
      * </p>
      * <p>
      * The order of the parameters is not defined (and not important). A missing
      * <code><u>parameterValue</u></code> indicates that the value of the
      * parameter is <code>null</code>.
      * </p>
      * <p>
      * For example, the string shown below represents a serialized parameterized
      * command that can be used to show the Resource perspective:
      * </p>
      * <p>
      * <code>org.eclipse.ui.perspectives.showPerspective(org.eclipse.ui.perspectives.showPerspective.perspectiveId=org.eclipse.ui.resourcePerspective)</code>
      * </p>
      * <p>
      * This example shows the more general form with multiple parameters,
      * <code>null</code> value parameters, and escaped <code>=</code> in the
      * third parameter value.
      * </p>
      * <p>
      * <code>command.id(param1.id=value1,param2.id,param3.id=esc%=val3)</code>
      * </p>
      *
      * @return A string containing the escaped command id, parameter ids and
      * parameter values; never <code>null</code>.
      * @see CommandManager#deserialize(String)
      * @since 3.2
      */
     public final String serialize() {
         final String escapedId = escape(getId());

         if ((parameterizations == null) || (parameterizations.length == 0)) {
             return escapedId;
         }

         final StringBuffer buffer = new StringBuffer (escapedId);
         buffer.append(CommandManager.PARAMETER_START_CHAR);

         for (int i = 0; i < parameterizations.length; i++) {

             if (i > 0) {
                 // insert separator between parameters
 buffer.append(CommandManager.PARAMETER_SEPARATOR_CHAR);
             }

             final Parameterization parameterization = parameterizations[i];
             final String parameterId = parameterization.getParameter().getId();
             final String escapedParameterId = escape(parameterId);

             buffer.append(escapedParameterId);

             final String parameterValue = parameterization.getValue();
             if (parameterValue != null) {
                 final String escapedParameterValue = escape(parameterValue);
                 buffer.append(CommandManager.ID_VALUE_CHAR);
                 buffer.append(escapedParameterValue);
             }
         }

         buffer.append(CommandManager.PARAMETER_END_CHAR);

         return buffer.toString();
     }

     public final String toString() {
         final StringBuffer buffer = new StringBuffer ();
         buffer.append("ParameterizedCommand("); //$NON-NLS-1$
 buffer.append(command);
         buffer.append(',');
         buffer.append(parameterizations);
         buffer.append(')');
         return buffer.toString();
     }
 }

